Skip to content

fix: refresh reused sandbox skills after local skill updates#6085

Open
stablegenius49 wants to merge 1 commit intoAstrBotDevs:masterfrom
stablegenius49:pr-factory/issue-5986-skill-refresh
Open

fix: refresh reused sandbox skills after local skill updates#6085
stablegenius49 wants to merge 1 commit intoAstrBotDevs:masterfrom
stablegenius49:pr-factory/issue-5986-skill-refresh

Conversation

@stablegenius49
Copy link
Contributor

@stablegenius49 stablegenius49 commented Mar 12, 2026

Summary

  • fingerprint the local skills tree and record the last synced revision on each sandbox booter
  • refresh sandbox skills when reusing an existing sandbox session whose local skills revision has changed
  • add regression coverage for revision stamping and reuse-time refresh behavior

Testing

  • PYTHONPATH=. pytest -q tests/test_computer_skill_sync.py tests/unit/test_computer.py::TestComputerClient::test_get_booter_refreshes_existing_session_when_skills_change tests/unit/test_computer.py::TestComputerClient::test_get_booter_skips_resync_when_skills_revision_matches

Closes #5986.

由 Sourcery 提供的总结

通过在每个 booter 上跟踪修订指纹,并在会话重用时使用它,确保当本地技能发生变化时会刷新沙盒会话。

错误修复:

  • 当本地技能树发生变化时,刷新现有的沙盒会话,而不是重用已过期的技能。

增强功能:

  • 为本地技能树生成指纹,并在成功完成技能同步后,将当前修订版本持久化到沙盒 booter 上,以实现更健壮的重用行为。

测试:

  • 添加单元测试,以验证当存储的技能修订版本与当前本地修订版本不同时会刷新 booter,而在两者匹配时则跳过刷新。
  • 添加回归测试,以确认成功的技能同步会将技能修订版本写入 booter。
Original summary in English

Summary by Sourcery

Ensure sandbox sessions are refreshed when local skills change by tracking a revision fingerprint on each booter and using it during session reuse.

Bug Fixes:

  • Refresh existing sandbox sessions when the local skills tree has changed instead of reusing stale skills.

Enhancements:

  • Fingerprint the local skills tree and persist the current revision on sandbox booters after successful skill syncs for more robust reuse behavior.

Tests:

  • Add unit tests verifying booters are refreshed when the stored skills revision differs from the current local revision and skipped when they match.
  • Add a regression test confirming that successful skill sync stamps the booter with a skills revision.

@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Mar 12, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses an issue where reused sandbox sessions might operate with outdated local skills. By introducing a robust skill revision tracking system, the changes ensure that any modifications to local skills are detected and automatically synchronized to the sandbox before it's reused, thereby maintaining consistency and preventing unexpected behavior due to stale skill sets.

Highlights

  • Local Skill Revision Tracking: Implemented a mechanism to compute a stable fingerprint (revision) for the local skills tree, allowing detection of changes to local skills.
  • Sandbox Skill Refresh on Reuse: Modified the sandbox booter reuse logic to automatically refresh sandbox skills if the local skills revision has changed since the last sync.
  • Regression Coverage: Added new test cases to ensure the correct behavior of skill revision stamping and the conditional refresh of sandbox skills upon reuse.
Changelog
  • astrbot/core/computer/computer_client.py
    • Imported the hashlib module for computing skill revisions.
    • Added _compute_local_skills_revision to generate a unique fingerprint for the local skills directory.
    • Introduced _get_booter_skills_revision and _set_booter_skills_revision to manage skill revision metadata on the ComputerBooter object.
    • Modified _sync_skills_to_sandbox to compute and store the current local skills revision after a successful sync.
    • Updated get_booter to check if the local skills revision has changed for an existing sandbox session and trigger a skill refresh if necessary.
  • tests/test_computer_skill_sync.py
    • Added test_sync_skills_sets_revision_after_success to verify that the skill revision is correctly set on the booter after a successful skill synchronization.
  • tests/unit/test_computer.py
    • Added test_get_booter_refreshes_existing_session_when_skills_change to confirm that get_booter refreshes a sandbox session when local skills have been updated.
    • Added test_get_booter_skips_resync_when_skills_revision_matches to ensure that get_booter skips redundant skill synchronization if the local skills revision matches the one stored on the booter.
Activity
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@dosubot dosubot bot added the area:core The bug / feature is about astrbot's core, backend label Mar 12, 2026
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 1 个问题,并留下了一些整体性的反馈:

  • _compute_local_skills_revision 中,建议考虑处理文件系统竞争条件(例如,在 rglobstat 之间文件被删除),以避免一次短暂的 FileNotFoundError 就导致整个修订版本计算失败。
  • 对于缺失的 skills 目录,目前修订版本语义不一致:_compute_local_skills_revision 返回 "missing",而在 skills 缺失时创建的 booter 从不会设置修订版本。这会导致每次复用时都进行不必要的重新同步。更干净的做法是在 _sync_skills_to_sandbox 的提前返回路径中也设置一个修订版本,或者统一处理 None"missing"
给 AI Agent 的提示词
Please address the comments from this code review:

## Overall Comments
- In `_compute_local_skills_revision`, consider handling filesystem races (e.g., files deleted between `rglob` and `stat`) so a transient `FileNotFoundError` doesn’t cause the whole revision computation to fail.
- The revision semantics for a missing skills directory are inconsistent (`_compute_local_skills_revision` returns 'missing' while booters created when skills are absent never get a revision set), which will cause a needless resync attempt on every reuse; it would be cleaner to set a revision even on the early-return path in `_sync_skills_to_sandbox` or normalize `None` vs 'missing'.

## Individual Comments

### Comment 1
<location path="tests/test_computer_skill_sync.py" line_range="77-86" />
<code_context>
+def test_sync_skills_sets_revision_after_success(monkeypatch, tmp_path: Path):
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen the revision assertion to check the exact expected value rather than just existence

This test currently only checks that `_astrbot_skills_revision` is truthy. Since `_compute_local_skills_revision` is deterministic for a given tree, it would be more robust to assert the exact revision value, e.g. by:

- Computing the expected revision via `computer_client._compute_local_skills_revision(Path(skills_root))` and comparing it to `booter._astrbot_skills_revision`, or
- Patching `_compute_local_skills_revision` to return a known sentinel (e.g. `"rev-123"`) and asserting the attribute matches it.

That way the test verifies that `_sync_skills_to_sandbox` propagates the correct revision, not just a non-empty value.

Suggested implementation:

```python
def test_sync_skills_sets_revision_after_success(monkeypatch, tmp_path: Path):
    skills_root = tmp_path / "skills"
    temp_root = tmp_path / "temp"
    skill_dir = skills_root / "custom-agent-skill"
    skill_dir.mkdir(parents=True, exist_ok=True)
    skill_dir.joinpath("SKILL.md").write_text("# demo", encoding="utf-8")
    temp_root.mkdir(parents=True, exist_ok=True)

    monkeypatch.setattr(
        "astrbot.core.computer.computer_client.get_astrbot_skills_path",
        lambda: str(skills_root),

```

```python
    expected_revision = computer_client._compute_local_skills_revision(skills_root)
    assert booter._astrbot_skills_revision == expected_revision

```

I’ve assumed the existing assertion is a simple truthiness check like `assert booter._astrbot_skills_revision` and that the test has `computer_client` and `booter` variables available in scope after invoking `_sync_skills_to_sandbox`. If the variable names differ (e.g. `client` instead of `computer_client`, or `skill_booter` instead of `booter`), adjust the `expected_revision = ...` line accordingly to use the correct instance. Also ensure the call to `_compute_local_skills_revision` uses the same type used elsewhere (e.g. `skills_root` as a `Path`; if the method expects a string, wrap with `str(skills_root)`).
</issue_to_address>

Sourcery 对开源项目是免费的——如果你觉得这些评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的评审。
Original comment in English

Hey - I've found 1 issue, and left some high level feedback:

  • In _compute_local_skills_revision, consider handling filesystem races (e.g., files deleted between rglob and stat) so a transient FileNotFoundError doesn’t cause the whole revision computation to fail.
  • The revision semantics for a missing skills directory are inconsistent (_compute_local_skills_revision returns 'missing' while booters created when skills are absent never get a revision set), which will cause a needless resync attempt on every reuse; it would be cleaner to set a revision even on the early-return path in _sync_skills_to_sandbox or normalize None vs 'missing'.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_compute_local_skills_revision`, consider handling filesystem races (e.g., files deleted between `rglob` and `stat`) so a transient `FileNotFoundError` doesn’t cause the whole revision computation to fail.
- The revision semantics for a missing skills directory are inconsistent (`_compute_local_skills_revision` returns 'missing' while booters created when skills are absent never get a revision set), which will cause a needless resync attempt on every reuse; it would be cleaner to set a revision even on the early-return path in `_sync_skills_to_sandbox` or normalize `None` vs 'missing'.

## Individual Comments

### Comment 1
<location path="tests/test_computer_skill_sync.py" line_range="77-86" />
<code_context>
+def test_sync_skills_sets_revision_after_success(monkeypatch, tmp_path: Path):
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen the revision assertion to check the exact expected value rather than just existence

This test currently only checks that `_astrbot_skills_revision` is truthy. Since `_compute_local_skills_revision` is deterministic for a given tree, it would be more robust to assert the exact revision value, e.g. by:

- Computing the expected revision via `computer_client._compute_local_skills_revision(Path(skills_root))` and comparing it to `booter._astrbot_skills_revision`, or
- Patching `_compute_local_skills_revision` to return a known sentinel (e.g. `"rev-123"`) and asserting the attribute matches it.

That way the test verifies that `_sync_skills_to_sandbox` propagates the correct revision, not just a non-empty value.

Suggested implementation:

```python
def test_sync_skills_sets_revision_after_success(monkeypatch, tmp_path: Path):
    skills_root = tmp_path / "skills"
    temp_root = tmp_path / "temp"
    skill_dir = skills_root / "custom-agent-skill"
    skill_dir.mkdir(parents=True, exist_ok=True)
    skill_dir.joinpath("SKILL.md").write_text("# demo", encoding="utf-8")
    temp_root.mkdir(parents=True, exist_ok=True)

    monkeypatch.setattr(
        "astrbot.core.computer.computer_client.get_astrbot_skills_path",
        lambda: str(skills_root),

```

```python
    expected_revision = computer_client._compute_local_skills_revision(skills_root)
    assert booter._astrbot_skills_revision == expected_revision

```

I’ve assumed the existing assertion is a simple truthiness check like `assert booter._astrbot_skills_revision` and that the test has `computer_client` and `booter` variables available in scope after invoking `_sync_skills_to_sandbox`. If the variable names differ (e.g. `client` instead of `computer_client`, or `skill_booter` instead of `booter`), adjust the `expected_revision = ...` line accordingly to use the correct instance. Also ensure the call to `_compute_local_skills_revision` uses the same type used elsewhere (e.g. `skills_root` as a `Path`; if the method expects a string, wrap with `str(skills_root)`).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +77 to +86
def test_sync_skills_sets_revision_after_success(monkeypatch, tmp_path: Path):
skills_root = tmp_path / "skills"
temp_root = tmp_path / "temp"
skill_dir = skills_root / "custom-agent-skill"
skill_dir.mkdir(parents=True, exist_ok=True)
skill_dir.joinpath("SKILL.md").write_text("# demo", encoding="utf-8")
temp_root.mkdir(parents=True, exist_ok=True)

monkeypatch.setattr(
"astrbot.core.computer.computer_client.get_astrbot_skills_path",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): 将修订版本的断言加强为检查精确的预期值,而不仅仅是检查其是否存在

当前这个测试只验证 _astrbot_skills_revision 是否为 truthy。由于 _compute_local_skills_revision 对于给定的目录树是确定性的,更稳健的做法是断言精确的修订版本值,例如:

  • 通过 computer_client._compute_local_skills_revision(Path(skills_root)) 计算期望的修订版本,并与 booter._astrbot_skills_revision 比较;或者
  • _compute_local_skills_revision 打补丁为返回一个已知的哨兵值(例如 "rev-123"),然后断言该属性与之相等。

这样测试可以验证 _sync_skills_to_sandbox 传播的是正确的修订版本,而不仅仅是一个非空值。

建议的实现:

def test_sync_skills_sets_revision_after_success(monkeypatch, tmp_path: Path):
    skills_root = tmp_path / "skills"
    temp_root = tmp_path / "temp"
    skill_dir = skills_root / "custom-agent-skill"
    skill_dir.mkdir(parents=True, exist_ok=True)
    skill_dir.joinpath("SKILL.md").write_text("# demo", encoding="utf-8")
    temp_root.mkdir(parents=True, exist_ok=True)

    monkeypatch.setattr(
        "astrbot.core.computer.computer_client.get_astrbot_skills_path",
        lambda: str(skills_root),
    expected_revision = computer_client._compute_local_skills_revision(skills_root)
    assert booter._astrbot_skills_revision == expected_revision

我假设当前的断言只是一个简单的 truthy 检查,例如 assert booter._astrbot_skills_revision,并且在调用 _sync_skills_to_sandbox 之后,测试中可以在作用域中访问到 computer_clientbooter 变量。如果变量名不同(例如使用 client 而不是 computer_client,或使用 skill_booter 而不是 booter),请相应调整 expected_revision = ... 这一行来使用正确的实例。另请确保调用 _compute_local_skills_revision 时使用与其他地方一致的类型(例如使用 Path 类型的 skills_root;如果该方法期望字符串类型,则用 str(skills_root) 包装)。

Original comment in English

suggestion (testing): Strengthen the revision assertion to check the exact expected value rather than just existence

This test currently only checks that _astrbot_skills_revision is truthy. Since _compute_local_skills_revision is deterministic for a given tree, it would be more robust to assert the exact revision value, e.g. by:

  • Computing the expected revision via computer_client._compute_local_skills_revision(Path(skills_root)) and comparing it to booter._astrbot_skills_revision, or
  • Patching _compute_local_skills_revision to return a known sentinel (e.g. "rev-123") and asserting the attribute matches it.

That way the test verifies that _sync_skills_to_sandbox propagates the correct revision, not just a non-empty value.

Suggested implementation:

def test_sync_skills_sets_revision_after_success(monkeypatch, tmp_path: Path):
    skills_root = tmp_path / "skills"
    temp_root = tmp_path / "temp"
    skill_dir = skills_root / "custom-agent-skill"
    skill_dir.mkdir(parents=True, exist_ok=True)
    skill_dir.joinpath("SKILL.md").write_text("# demo", encoding="utf-8")
    temp_root.mkdir(parents=True, exist_ok=True)

    monkeypatch.setattr(
        "astrbot.core.computer.computer_client.get_astrbot_skills_path",
        lambda: str(skills_root),
    expected_revision = computer_client._compute_local_skills_revision(skills_root)
    assert booter._astrbot_skills_revision == expected_revision

I’ve assumed the existing assertion is a simple truthiness check like assert booter._astrbot_skills_revision and that the test has computer_client and booter variables available in scope after invoking _sync_skills_to_sandbox. If the variable names differ (e.g. client instead of computer_client, or skill_booter instead of booter), adjust the expected_revision = ... line accordingly to use the correct instance. Also ensure the call to _compute_local_skills_revision uses the same type used elsewhere (e.g. skills_root as a Path; if the method expects a string, wrap with str(skills_root)).

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively addresses the issue of stale skills in reused sandbox sessions by introducing a revisioning system based on file metadata. The implementation is sound and is accompanied by appropriate regression tests. My review includes a critical fix for a potential hash collision in the revision computation, an improvement to error logging for better observability, and a refactoring suggestion for the new tests to enhance maintainability.

Comment on lines +52 to +57
relative = path.relative_to(skills_root).as_posix()
digest.update(relative.encode("utf-8"))
stat = path.stat()
digest.update(str(stat.st_mtime_ns).encode("utf-8"))
if path.is_file():
digest.update(str(stat.st_size).encode("utf-8"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current method of updating the hash digest by concatenating file metadata without separators can lead to hash collisions. For example, a file with relative path foo and mtime 12345 would produce the same hash input as a file with path foo1 and mtime 2345, as both result in the byte sequence foo12345 being hashed. This could cause the system to fail to detect changes in skills. Using a separator like a null byte between each piece of metadata will prevent such ambiguities.

Suggested change
relative = path.relative_to(skills_root).as_posix()
digest.update(relative.encode("utf-8"))
stat = path.stat()
digest.update(str(stat.st_mtime_ns).encode("utf-8"))
if path.is_file():
digest.update(str(stat.st_size).encode("utf-8"))
relative = path.relative_to(skills_root).as_posix()
stat = path.stat()
digest.update(relative.encode("utf-8"))
digest.update(b"\0")
digest.update(str(stat.st_mtime_ns).encode("utf-8"))
digest.update(b"\0")
if path.is_file():
digest.update(str(stat.st_size).encode("utf-8"))
digest.update(b"\0")

Comment on lines +488 to +493
except Exception as e:
logger.warning(
"Failed to refresh sandbox skills before reusing session %s: %s",
session_id,
e,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Catching a broad Exception is reasonable here to prevent crashes, but logging just str(e) can hide important debugging information like the traceback. Using logger.warning with exc_info=True will automatically include the exception information and traceback in the log, which is much more helpful for diagnosing failures during skill synchronization.

Suggested change
except Exception as e:
logger.warning(
"Failed to refresh sandbox skills before reusing session %s: %s",
session_id,
e,
)
except Exception:
logger.warning(
"Failed to refresh sandbox skills before reusing session %s",
session_id,
exc_info=True,
)

Comment on lines +736 to +821
async def test_get_booter_refreshes_existing_session_when_skills_change(self):
"""Test get_booter re-syncs local skills before reusing a stale sandbox."""
from astrbot.core.computer import computer_client

computer_client.session_booter.clear()

mock_context = MagicMock()
mock_config = MagicMock()
mock_config.get = lambda key, default=None: {
"provider_settings": {
"computer_use_runtime": "sandbox",
"sandbox": {
"booter": "shipyard",
"shipyard_endpoint": "http://localhost:8080",
"shipyard_access_token": "test_token",
}
}
}.get(key, default)
mock_context.get_config = MagicMock(return_value=mock_config)

mock_booter = MagicMock()
mock_booter.available = AsyncMock(return_value=True)
mock_booter._astrbot_skills_revision = "rev-old"

sync_mock = AsyncMock()
with (
patch(
"astrbot.core.computer.computer_client._compute_local_skills_revision",
return_value="rev-new",
),
patch(
"astrbot.core.computer.computer_client._sync_skills_to_sandbox",
sync_mock,
),
):
computer_client.session_booter["test-session"] = mock_booter

booter = await computer_client.get_booter(mock_context, "test-session")
assert booter is mock_booter
sync_mock.assert_awaited_once_with(mock_booter)

computer_client.session_booter.clear()

@pytest.mark.asyncio
async def test_get_booter_skips_resync_when_skills_revision_matches(self):
"""Test get_booter reuses existing booter without redundant syncs."""
from astrbot.core.computer import computer_client

computer_client.session_booter.clear()

mock_context = MagicMock()
mock_config = MagicMock()
mock_config.get = lambda key, default=None: {
"provider_settings": {
"computer_use_runtime": "sandbox",
"sandbox": {
"booter": "shipyard",
"shipyard_endpoint": "http://localhost:8080",
"shipyard_access_token": "test_token",
}
}
}.get(key, default)
mock_context.get_config = MagicMock(return_value=mock_config)

mock_booter = MagicMock()
mock_booter.available = AsyncMock(return_value=True)
mock_booter._astrbot_skills_revision = "rev-same"

sync_mock = AsyncMock()
with (
patch(
"astrbot.core.computer.computer_client._compute_local_skills_revision",
return_value="rev-same",
),
patch(
"astrbot.core.computer.computer_client._sync_skills_to_sandbox",
sync_mock,
),
):
computer_client.session_booter["test-session"] = mock_booter

booter = await computer_client.get_booter(mock_context, "test-session")
assert booter is mock_booter
sync_mock.assert_not_awaited()

computer_client.session_booter.clear()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The tests test_get_booter_refreshes_existing_session_when_skills_change and test_get_booter_skips_resync_when_skills_revision_matches are nearly identical, with only the revision values and the expected outcome being different. This code duplication can be eliminated by using pytest.mark.parametrize to create a single, data-driven test. This will make the test suite more concise and easier to maintain or extend in the future.

    @pytest.mark.asyncio
    @pytest.mark.parametrize(
        "old_rev, new_rev, should_sync",
        [
            ("rev-old", "rev-new", True),
            ("rev-same", "rev-same", False),
        ],
        ids=[
            "refreshes_when_skills_change",
            "skips_resync_when_skills_revision_matches",
        ],
    )
    async def test_get_booter_skill_refresh_logic(self, old_rev, new_rev, should_sync):
        """Test get_booter re-syncs local skills based on revision changes."""
        from astrbot.core.computer import computer_client

        computer_client.session_booter.clear()

        mock_context = MagicMock()
        mock_config = MagicMock()
        mock_config.get = lambda key, default=None: {
            "provider_settings": {
                "computer_use_runtime": "sandbox",
                "sandbox": {
                    "booter": "shipyard",
                    "shipyard_endpoint": "http://localhost:8080",
                    "shipyard_access_token": "test_token",
                },
            }
        }.get(key, default)
        mock_context.get_config = MagicMock(return_value=mock_config)

        mock_booter = MagicMock()
        mock_booter.available = AsyncMock(return_value=True)
        mock_booter._astrbot_skills_revision = old_rev

        sync_mock = AsyncMock()
        with (
            patch(
                "astrbot.core.computer.computer_client._compute_local_skills_revision",
                return_value=new_rev,
            ),
            patch(
                "astrbot.core.computer.computer_client._sync_skills_to_sandbox",
                sync_mock,
            ),
        ):
            computer_client.session_booter["test-session"] = mock_booter

            booter = await computer_client.get_booter(mock_context, "test-session")
            assert booter is mock_booter
            if should_sync:
                sync_mock.assert_awaited_once_with(mock_booter)
            else:
                sync_mock.assert_not_awaited()

        computer_client.session_booter.clear()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core The bug / feature is about astrbot's core, backend size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] skill更新无法在已存在的shipyard实例中更新

1 participant